using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Media3D;


namespace TicTacToe._3D
{
    sealed class Ball : ModelVisual3D
    {
        private readonly GeometryModel3D mGeoModel;

        public static readonly DependencyProperty PositionProperty =
            DependencyProperty.Register("Position", typeof (Point3D), typeof (Ball),
                                        new PropertyMetadata(new Point3D(0, 0, 0), PositionChanged));

        public static readonly DependencyProperty RadiusProperty =
            DependencyProperty.Register("Radius", typeof (double), typeof (Ball),
                                        new PropertyMetadata(1D, RadiusChanged));

        public static readonly DependencyProperty MaterialProperty =
            GeometryModel3D.MaterialProperty.AddOwner(typeof (Ball), new PropertyMetadata(MaterialChanged));

        public Point3D Position
        {
            get { return (Point3D)GetValue(PositionProperty); }
            set { SetValue(PositionProperty, value); }
        }

        public double Radius
        {
            get { return (double)GetValue(RadiusProperty); }
            set { SetValue(RadiusProperty, value); }
        }

        public Material Material
        {
            get { return (Material)GetValue(MaterialProperty); }
            set { SetValue(MaterialProperty, value); }
        }

        public Ball()
            :this(1, new Point3D(0,0,0))
        {
        }

        public Ball(double radius, Point3D position)
        {
            mGeoModel = new GeometryModel3D {Geometry = Tessellate()};
            Content = mGeoModel;

            Content.Transform = new Transform3DGroup();

            Position = position;
            Radius = radius;
        }

        private static void MaterialChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var ball = (Ball) d;

            if (e.Property == MaterialProperty)
                ball.mGeoModel.Material = e.NewValue as Material;
        }

        private static void PositionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var ball = (Ball) d;
            var vector = (Point3D)e.NewValue - (Point3D) e.OldValue;
            var translate = new TranslateTransform3D(vector);

            ((Transform3DGroup) ball.mGeoModel.Transform).Children.Add(translate);
        }

        private static void RadiusChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var ball = (Ball) d;

            var scFactor = 0.5*(1/((double) e.OldValue/(double) e.NewValue));
            var scale = new ScaleTransform3D(new Vector3D(scFactor, scFactor, scFactor), ball.Position);
            ((Transform3DGroup)ball.mGeoModel.Transform).Children.Add(scale);
        }

        private static Point3D GetPosition(double t, double y)
        {
            var r = Math.Sqrt(1 - y*y);
            var x = r * Math.Cos(t);
            var z = r * Math.Sin(t);

            return new Point3D(x, y, z);
        }

        private static Vector3D GetNormal(double t, double y)
        {
            return (Vector3D) GetPosition(t, y);
        }

        private static Point GetTextureCoordinate(double t, double y)
        {
            var TYtoUV = new Matrix();
            TYtoUV.Scale(1/(2*Math.PI), -0.5);

            var p = new Point(t, y);
            p = p*TYtoUV;

            return p;
        }

        private static Geometry3D Tessellate()
        {
            const int tDiv = 32;
            const int yDiv = 32;
            const double maxTheta = 2 * Math.PI;
            const double minY = -1.0;
            const double maxY = 1.0;

            const double dt = maxTheta / tDiv;
            const double dy = (maxY - minY) / yDiv;

            var mesh = new MeshGeometry3D();

            for (var yi = 0; yi <= yDiv; yi++)
            {
                var y = minY + yi*dy;

                for (var ti = 0; ti <= tDiv; ti++)
                {
                    var t = ti*dt;

                    mesh.Positions.Add(GetPosition(t, y));
                    mesh.Normals.Add(GetNormal(t, y));
                    mesh.TextureCoordinates.Add(GetTextureCoordinate(t, y));
                }
            }

            for (var yi = 0; yi < yDiv; yi++)
            {
                for (var ti = 0; ti < tDiv; ti++)
                {
                    var x0 = ti;
                    var x1 = (ti + 1);
                    var y0 = yi * (tDiv + 1);
                    var y1 = (yi + 1) * (tDiv + 1);

                    mesh.TriangleIndices.Add(x0 + y0);
                    mesh.TriangleIndices.Add(x0 + y1);
                    mesh.TriangleIndices.Add(x1 + y0);

                    mesh.TriangleIndices.Add(x1 + y0);
                    mesh.TriangleIndices.Add(x0 + y1);
                    mesh.TriangleIndices.Add(x1 + y1);
                }
            }

            mesh.Freeze();
            return mesh;
        }
    }
}